//
// Created by 郝羽 on 2018/6/6.
//

//
// Created by 郝羽 on 2018/3/22.
//
#include <cstdio>
#include <glib.h>
#include <gio/gio.h>
#include <netinet/in.h>
#include <set>

constexpr int BUFFER_SIZE = 65536;

struct EchoServer;

struct EchoConnection {
    GSocket* socket;
    guint read_event;
    guint write_event;
    char* buf;
    int len;
    int cap;
    EchoServer* server;
};

struct EchoServer {
    GSocket* socket;
    guint new_connection_event;
    std::set<EchoConnection*> connections;
};


void echoConnectionMakeSpace(EchoConnection* connection) {
    gsize cap = connection->cap * 2;
    printf("g_realloc(%p, %u)\n", connection->buf, cap);
    connection->buf = (char*)g_realloc(connection->buf, cap);
    connection->cap = cap;
}

gboolean echoConnectionHandleWrite(GIOChannel* ch, GIOCondition cond, gpointer data) {
    auto connection = static_cast<EchoConnection*>(data);

    auto len = connection->len;

    gsize write = 0;

    GError* err = nullptr;

    g_io_channel_write_chars(ch, connection->buf, len, &write, &err);

    if (err != nullptr) {
        printf("could not write: %s", err->message);
        g_error_free(err);
        return false;
    }

    printf("write %d bytes\n", write);

    connection->len -= write;

    if (connection->len > 0) {
        memmove(connection->buf, connection->buf + write, len - write);
        return true;
    }

    connection->write_event = 0;
    return false;
}

void deleteEchoConnection(EchoConnection* connection) {
    if (connection == nullptr) {
        return;
    }

    if (connection->socket) {
        if (g_socket_is_closed(connection->socket)) {
            g_socket_close(connection->socket, nullptr);
        }
        g_object_unref(connection->socket);
    }

    if (connection->read_event) {
        g_source_remove(connection->read_event);
        connection->read_event = 0;
    }

    if (connection->write_event) {
        g_source_remove(connection->write_event);
        connection->write_event = 0;
    }

    if (connection->buf) {
        g_free(connection->buf);
        connection->buf = nullptr;
    }

    delete connection;
}

gboolean echoConnectionHandleRead(GIOChannel* ch, GIOCondition cond, gpointer data) {
    auto connection = static_cast<EchoConnection*>(data);

reread:
    gsize avail = connection->cap - connection->len;

    gsize read = 0;

    GError* err = nullptr;


    g_io_channel_read_chars(ch, connection->buf + connection->len, avail, &read, &err);

    if (err != nullptr) {
        printf("could not read: %s", err->message);
        g_error_free(err);
        return false;
    }

    if (read == 0) {
        printf("connection closed\n");

        connection->server->connections.erase(connection);

        deleteEchoConnection(connection);

        return false;
    }

    connection->len += read;

    printf("read %d bytes, buf %d bytes\n", read, connection->len);

    if (read == avail) {
        echoConnectionMakeSpace(connection);
        goto reread;
    }

    if (connection->write_event == 0) {
        auto out_ch = g_io_channel_unix_new(g_socket_get_fd(connection->socket));

        connection->write_event = g_io_add_watch(out_ch, G_IO_OUT, echoConnectionHandleWrite, connection);

        g_io_channel_unref(out_ch);
    }

    return true;
}



EchoConnection* newEchoConnection(GSocket* socket) {
    auto connection = new EchoConnection();

    connection->socket = socket;

    auto ch = g_io_channel_unix_new(g_socket_get_fd(socket));

    if (g_io_channel_get_encoding(ch) != nullptr) {
        printf("io channel encoding is '%s'\n", g_io_channel_get_encoding(ch));
        GError* err = nullptr;
        g_io_channel_set_encoding(ch, nullptr, &err);
        if (err != nullptr) {
            printf("could not clear io channel encoding\n");
        }
    }
    if (g_io_channel_get_buffered(ch) == TRUE) {
        printf("io channel is buffered\n");
        g_io_channel_set_buffered(ch, FALSE);
    }

    connection->read_event = g_io_add_watch(ch, G_IO_IN, echoConnectionHandleRead, connection);

    g_io_channel_unref(ch);

    connection->buf = (char*)g_malloc(1024);

    connection->len = 0;

    connection->cap = 1024;



    return connection;
}

gboolean echoServerHandleNewConnection(GIOChannel* ch, GIOCondition cond, gpointer data) {
    auto server = static_cast<EchoServer*>(data);

    GError* err = nullptr;

    auto new_socket = g_socket_accept(server->socket, nullptr, &err);

    if (err != nullptr) {
        printf("could not accept new socket: %s\n", err->message);
        g_error_free(err);
        return false;
    }

    if (g_socket_get_blocking(new_socket) == TRUE) {
        printf("set socket non-blocking\n");
        g_socket_set_blocking(new_socket, FALSE);
    }


    auto connection = newEchoConnection(new_socket);
    if (!connection) {
        printf("could not create new connection\n");
        return false;
    }

    printf("new connection\n");

    server->connections.emplace(connection);

    connection->server = server;

    return true;
}

void deleteEchoServer(EchoServer* server) {
    if (server == nullptr) {
        return;
    }

    if (server->socket != nullptr) {
        if (!g_socket_is_closed(server->socket)) {
            g_socket_close(server->socket, nullptr);
        }
        g_object_unref(server->socket);
    }

    if (server->new_connection_event > 0) {
        g_source_remove(server->new_connection_event);
        server->new_connection_event = 0;
    }

    for (auto conn : server->connections) {
        deleteEchoConnection(conn);
    }

    delete(server);
}

EchoServer* newEchoServer(int port) {
    auto server = new EchoServer();

    GError* err;
    server->socket = g_socket_new(G_SOCKET_FAMILY_IPV4, G_SOCKET_TYPE_STREAM, G_SOCKET_PROTOCOL_TCP, &err);
    if (!server->socket) {
        fprintf(stderr, "could not create socket: %s", err->message);
        g_error_free(err);
        deleteEchoServer(server);
        return nullptr;
    }

    GSocketAddress* local_addr = G_SOCKET_ADDRESS(g_inet_socket_address_new(g_inet_address_new_any(G_SOCKET_FAMILY_IPV4), port));
    if (g_socket_bind(server->socket, local_addr, TRUE, &err) == FALSE) {
        fprintf(stderr, "could not bind to port %d: %s", port, err->message);
        g_error_free(err);
        deleteEchoServer(server);
        return nullptr;
    }

    if (g_socket_listen(server->socket, &err) == FALSE) {
        fprintf(stderr, "could not listen to port %d: %s", port, err->message);
        g_error_free(err);
        deleteEchoServer(server);
        return nullptr;
    }

    GIOChannel* ch = g_io_channel_unix_new(g_socket_get_fd(server->socket));

    server->new_connection_event = g_io_add_watch(ch, G_IO_IN, echoServerHandleNewConnection, server);

    g_io_channel_unref(ch);

    return server;
}

int main(int argc, const char* argv[]) {
    int port = 10000;
    if (argc > 1) {
        port = atoi(argv[1]);
    }

    auto server = newEchoServer(port);

    GMainLoop * gloop = g_main_loop_new(nullptr, FALSE);
    g_main_loop_run(gloop);

    deleteEchoServer(server);
}